<?php

class CCoreNetImapBodyStructure
{
	#<editor-fold defaultstate="collapsed" desc="private">
	/**
	 * @var string
	 */
	private $_sContentType;

	/**
	 * @var string
	 */
	private $_sCharset;

	/**
	 * @var string
	 */
	private $_sCharsetEncoding;

	/**
	 * @var array
	 */
	private $_aBodyParams;

	/**
	 * @var string
	 */
	private $_sContentID;

	/**
	 * @var string
	 */
	private $_sDescription;

	/**
	 * @var string
	 */
	private $_sMailEncodingName;

	/**
	 * @var string
	 */
	private $_sDisposition;

	/**
	 * @var array
	 */
	private $_aDispositionParams;

	/**
	 * @var string
	 */
	private $_sFileName;

	/**
	 * @var string
	 */
	private $_sLanguage;

	/**
	 * @var string
	 */
	private $_sLocation;

	/**
	 * @var int
	 */
	private $_iSize;

	/**
	 * @var int
	 */
	private $_iTextLineCount;

	/**
	 * @var string
	 */
	private $_sPartID;

	/**
	 * @var array
	 */
	private $_aSubParts;
	#</editor-fold>

	/**
	 * @param string $sContentType
	 * @param string $sCharset
	 * @param string $sCharsetEncoding
	 * @param array $aBodyParams
	 * @param string $sContentID
	 * @param string $sDescription
	 * @param string $sMailEncodingName
	 * @param string $sDisposition
	 * @param array $aDispositionParams
	 * @param string $sFileName
	 * @param string $sLanguage
	 * @param string $sLocation
	 * @param int $iSize
	 * @param int $iTextLineCount
	 * @param string $sPartID
	 * @param array $aSubParts
	 * @return CCoreNetImapBodyStructure
	 */
	public function __construct($sContentType, $sCharset, $sCharsetEncoding, $aBodyParams, $sContentID,
		$sDescription, $sMailEncodingName, $sDisposition, $aDispositionParams, $sFileName,
		$sLanguage, $sLocation, $iSize, $iTextLineCount, $sPartID, $aSubParts)
	{
		$this->_sContentType = $sContentType;
		$this->_sCharset = $sCharset;
		$this->_sCharsetEncoding = $sCharsetEncoding;
		$this->_aBodyParams = $aBodyParams;
		$this->_sContentID = $sContentID;
		$this->_sDescription = $sDescription;
		$this->_sMailEncodingName = $sMailEncodingName;
		$this->_sDisposition = $sDisposition;
		$this->_aDispositionParams = $aDispositionParams;
		$this->_sFileName = $sFileName;
		$this->_sLanguage = $sLanguage;
		$this->_sLocation = $sLocation;
		$this->_iSize = $iSize;
		$this->_iTextLineCount = $iTextLineCount;
		$this->_sPartID = $sPartID;
		$this->_aSubParts = $aSubParts;
	}

	/**
	 * @return CCoreNetImapBodyStructure
	 */
	public function SearchPlainPart()
	{
		$oReturn = null;
		$aParts = $this->SearchByContentType('text/plain');
		foreach ($aParts as $oPart)
		{
			if (!$oPart->_isAttachBodyPart())
			{
				$oReturn = $oPart;
			}
		}
		return $oReturn;
	}

	/**
	 * @return CCoreNetImapBodyStructure
	 */
	public function SearchHtmlPart()
	{
		$oReturn = null;
		$aParts = $this->SearchByContentType('text/html');
		foreach ($aParts as $oPart)
		{
			if (!$oPart->_isAttachBodyPart())
			{
				$oReturn = $oPart;
			}
		}
		return $oReturn;
	}

	/**
	 * @return bool
	 */
	private function _isAttachBodyPart()
	{
		return (
			(null !== $this->_sFileName && 0 < strlen($this->_sFileName))
			|| (null !== $this->_sDisposition && 'attachment' === strtolower($this->_sDisposition))
			|| (null !== $this->_sContentType &&
				('message/rfc822' === strtolower($this->_sContentType) ||
					'image/' === substr(strtolower($this->_sContentType), 0, 6)))
		);
	}

	/**
	 * @return array
	 */
	public function SearchAttachmentsParts()
	{
		$aReturn = array();
		if ($this->_isAttachBodyPart())
		{
			$aReturn[] = $this;
		}

		if (is_array($this->_aSubParts) && 0 < count($this->_aSubParts))
		{
			foreach ($this->_aSubParts as /* @var $oSubPart CCoreNetImapBodyStructure */ &$oSubPart)
			{
				$aReturn = array_merge($aReturn, $oSubPart->SearchAttachmentsParts());
			}
		}

		return $aReturn;
	}

	/**
	 * @param string $sContentType
	 * @return array
	 */
	public function SearchByContentType($sContentType)
	{
		$aReturn = array();
		if (strtolower($sContentType) === $this->_sContentType)
		{
			$aReturn[] = $this;
		}
		
		if (is_array($this->_aSubParts) && 0 < count($this->_aSubParts))
		{
			foreach ($this->_aSubParts as /* @var $oSubPart CCoreNetImapBodyStructure */ &$oSubPart)
			{
				$aReturn = array_merge($aReturn, $oSubPart->SearchByContentType($sContentType));
			}
		}
		
		return $aReturn;
	}

	/**
	 * return string
	 */
	public function MailEncodingName()
	{
		return $this->_sMailEncodingName;
	}

	/**
	 * return string
	 */
	public function PartID()
	{
		return $this->_sPartID;
	}

	/**
	 * return string
	 */
	public function FileName()
	{
		return $this->_sFileName;
	}

	/**
	 * return string
	 */
	public function ContentType()
	{
		return $this->_sContentType;
	}

	/**
	 * return int
	 */
	public function Size()
	{
		return $this->_iSize;
	}

	/**
	 * return int
	 */
	public function EstimatedSize()
	{
		$fCoefficient = 1;
		switch (strtolower($this->MailEncodingName()))
		{
			case 'base64':
				$fCoefficient = 0.75;
				break;
			case 'quoted-printable':
				$fCoefficient = 0.44;
				break;
		}

		return (int) ($this->Size() * $fCoefficient);
	}

	/**
	 * return string
	 */
	public function Charset()
	{
		return $this->_sCharset;
	}

	/**
	 * return string
	 */
	public function CharsetEncoding()
	{
		return $this->_sCharsetEncoding;
	}
	
	/**
	 * return string
	 */
	public function ContentID()
	{
		return (null === $this->_sContentID) ? '' : trim($this->_sContentID, '<>');
	}

	/**
	 * @param array $aBodyStructure
	 * @param string $sEnc
	 * @param string $sPartID = ''
	 * @return ImapBodyStructure
	 */
	public static function Parse(array $aBodyStructure, $sEnc = C_MIME_CHARSET_UTF8, $sPartID = '')
	{
		if (!is_array($aBodyStructure) || 2 > count($aBodyStructure))
		{
			return null;
		}
		else
		{
			$sBodyMainType = null;
			if (is_string($aBodyStructure[0]) && 'NIL' !== $aBodyStructure[0])
			{
				$sBodyMainType = $aBodyStructure[0];
			}

			$sBodySubType = null;
			$sContentType = '';
			$aSubParts = null;
			$aBodyParams = array();
			$sCharset = null;
			$sContentID = null;
			$sDescription = null;
			$sMailEncodingName = null;
			$iSize = 0;
			$iTextLineCount = 0; // valid for rfc822/message and text parts
			$iExtraItemPos = 0;  // list index of items which have no well-established position (such as 0, 1, 5, etc).

			if (null === $sBodyMainType)
			{
				// Process multipart body structure
				if (!is_array($aBodyStructure[0]))
				{
					return null;
				}
				else
				{
					$sBodyMainType = 'multipart';
					$sSubPartIDPrefix = '';
					if (empty($sPartID) || '.' === $sPartID[strlen($sPartID) - 1])
					{
						// This multi-part is root part of message.
						$sSubPartIDPrefix = $sPartID;
						$sPartID .= 'TEXT';
					}
					else if (!empty($sPartID))
					{
						// This multi-part is a part of another multi-part.
						$sSubPartIDPrefix = $sPartID.'.';
					}

					$aSubParts = array();
					$i = 1;

					while ($iExtraItemPos < count($aBodyStructure) && is_array($aBodyStructure[$iExtraItemPos]))
					{
						$oPart = self::Parse($aBodyStructure[$iExtraItemPos], $sEnc, $sSubPartIDPrefix.$i);
						if (null === $oPart)
						{
							return null;
						}
						else
						{
							// For multipart, we have no charset info in the part itself. Thus,
							// obtain charset from nested parts.
							if ($sCharset == null)
							{
								$sCharset = $oPart->Charset();
								if (null !== $sCharset)
								{
									$sEnc = $oPart->CharsetEncoding();
								}
							}
							$aSubParts[] = $oPart;
							$iExtraItemPos++;
							$i++;
						}
					}
				}


				if ($iExtraItemPos < count($aBodyStructure))
				{
					if (!is_string($aBodyStructure[$iExtraItemPos]) || 'NIL' === $aBodyStructure[$iExtraItemPos])
					{
						return null;
					}

					$sBodySubType = strtolower($aBodyStructure[$iExtraItemPos]);
					$iExtraItemPos++;
				}

				if ($iExtraItemPos < count($aBodyStructure))
				{
					$sBodyParamList = $aBodyStructure[$iExtraItemPos];
					if (is_array($sBodyParamList))
					{
						$aBodyParams = self::GetKeyValueListFromArrayList($sBodyParamList, $sEnc);
					}
				}
				$iExtraItemPos++;
			}
			else
			{
				// Process simple (singlepart) body structure
				if (7 > count($aBodyStructure))
				{
					return null;
				}

				$sBodyMainType = strtolower($sBodyMainType);
				if (!is_string($aBodyStructure[1]) || 'NIL' === $aBodyStructure[1])
				{
					return null;
				}
				
				$sBodySubType = strtolower($aBodyStructure[1]);

				$aBodyParamList = $aBodyStructure[2];
				if (is_array($aBodyParamList))
				{
					$aBodyParams = self::GetKeyValueListFromArrayList($aBodyParamList, $sEnc);
					if (isset($aBodyParams['charset']))
					{
						$sCharset = $aBodyParams['charset'];
					}
					if (null !== $sCharset)
					{
						$sEnc = $sCharset;
					}
				}
				
				if (null !== $aBodyStructure[3] && 'NIL' !== $aBodyStructure[3])
				{
					if (!is_string($aBodyStructure[3]))
					{
						return null;
					}
					$sContentID = $aBodyStructure[3];
				}

				if (null !== $aBodyStructure[4] && 'NIL' !== $aBodyStructure[4])
				{
					if (!is_string($aBodyStructure[4]))
					{
						return null;
					}
					$sDescription = $aBodyStructure[4];
				}

				if (null !== $aBodyStructure[5] && 'NIL' !== $aBodyStructure[5])
				{
					if (!is_string($aBodyStructure[5]))
					{
						return null;
					}
					$sMailEncodingName = $aBodyStructure[5];
				}

				if (is_numeric($aBodyStructure[6]))
				{
					$iSize = (int) $aBodyStructure[6];
				}
				else
				{
					$iSize = -1;
				}

				if (empty($sPartID) || '.' === $sPartID[strlen($sPartID) - 1])
				{
					// This is the only sub-part of the message (otherwise, it would be
					// one of sub-parts of a multi-part, and partID would already be fully set up).
					$sPartID .= '1';
				}

				$iExtraItemPos = 7;
				if ('text' === $sBodyMainType)
				{
					if ($iExtraItemPos < count($aBodyStructure))
					{
						if (is_numeric($aBodyStructure[$iExtraItemPos]))
						{
							$iTextLineCount = (int) $aBodyStructure[$iExtraItemPos];
						}
						else
						{
							$iTextLineCount = -1;
						}
					}
					else
					{
						$iTextLineCount = -1;
					}
					$iExtraItemPos++;
				}
				else if ('message' === $sBodyMainType && 'rfc822' === $sBodySubType)
				{
					if ($iExtraItemPos + 2 < count($aBodyStructure))
					{
						if (is_numeric($aBodyStructure[$iExtraItemPos + 2]))
						{
							$iTextLineCount = (int) $aBodyStructure[$iExtraItemPos + 2];
						}
						else
						{
							$iTextLineCount = -1;
						}
					}
					else
					{
						$iTextLineCount = -1;
					}
					$iExtraItemPos += 3;
				}
				
				$iExtraItemPos++;	// skip MD5 digest of the body because most mail servers leave it NIL anyway
			}

			$sContentType = $sBodyMainType.'/'.$sBodySubType;

			$sDisposition = null;
			$aDispositionParams = null;
			$sFileName = null;

			if ($iExtraItemPos < count($aBodyStructure))
			{
				$aDispList = $aBodyStructure[$iExtraItemPos];
				if (is_array($aDispList) && 1 < count($aDispList))
				{
					if (null !== $aDispList[0])
					{
						if (is_string($aDispList[0]) && 'NIL' !== $aDispList[0])
						{
							$sDisposition = $aDispList[0];
						}
						else
						{
							return null;
						}
					}
				}

				$aDispParamList = $aDispList[1];
				if (is_array($aDispParamList))
				{
					$aDispositionParams = self::GetKeyValueListFromArrayList($aDispParamList, $sEnc);
					if (isset($aDispositionParams['filename']))
					{
						$sFileName = CCoreMimeUtils::DecodeHeaderValue($aDispositionParams['filename'],
							$sEnc, C_MIME_CHARSET_UTF8);
					}
				}
			}
			$iExtraItemPos++;

			$sLanguage = null;
			if ($iExtraItemPos < count($aBodyStructure))
			{
				if (null !== $aBodyStructure[$iExtraItemPos] && 'NIL' !== $aBodyStructure[$iExtraItemPos])
				{
					if (is_array($aBodyStructure[$iExtraItemPos]))
					{
						$sLanguage = implode(',', $aBodyStructure[$iExtraItemPos]);
					}
					else if (is_string($aBodyStructure[$iExtraItemPos]))
					{
						$sLanguage = $aBodyStructure[$iExtraItemPos];
					}
				}
				$iExtraItemPos++;
			}

			$sLocation = null;
			if ($iExtraItemPos < count($aBodyStructure))
			{
				if (null !== $aBodyStructure[$iExtraItemPos] && 'NIL' !== $aBodyStructure[$iExtraItemPos])
				{
					if (is_string($aBodyStructure[$iExtraItemPos]))
					{
						$sLocation = $aBodyStructure[$iExtraItemPos];
					}
				}
				$iExtraItemPos++;
			}

			return new CCoreNetImapBodyStructure(
					$sContentType,
					$sCharset,
					$sEnc,
					$aBodyParams,
					$sContentID,
					$sDescription,
					$sMailEncodingName,
					$sDisposition,
					$aDispositionParams,
					$sFileName,
					$sLanguage,
					$sLocation,
					$iSize,
					$iTextLineCount,
					$sPartID,
					$aSubParts
				);
		}
	}

	/**
	 * Returns dict with key="charset" and value="US-ASCII" for array ("CHARSET" "US-ASCII").
	 * Keys are lowercased (StringDictionary itself does this), values are not altered.
	 * 
	 * @param array $aList
	 * @param string $sEnc
	 * @return array
	 */
	private static function GetKeyValueListFromArrayList(array $aList, $sEnc)
	{
		if (0 !== count($aList) % 2)
		{
			return null; // odd number of elements not allowed
		}
		else
		{
			$aDict = array();
			for ($i = 0, $c = count($aList); $i < $c; $i += 2)
			{
				if (is_string($aList[$i]) && isset($aList[$i + 1]) && is_string($aList[$i + 1]))
				{
					$aDict[strtolower($aList[$i])] = $aList[$i + 1];
				}
			}
			return $aDict;
		}
	}
}
